package battlezone;

import java.util.*;

/**
 * The model
 * The model periodically redraws the graphics when it detects that the graphics have gone out of date.
 */
public class Model {
    private View _view;
    private Network _network;
    private static final Configuration _configuration = Configuration.getInstance();
    private static final Random _random = new Random();
    private Tank _myTank;
    private String _myTankID;
    private final java.util.concurrent.ConcurrentHashMap<String, BattlezoneObject> _othersTanksAndAllProjectiles = new java.util.concurrent.ConcurrentHashMap<String, BattlezoneObject>();
    private final Timer _timer = new Timer();
    private boolean _modelChanged; //since last display refresh
    private short _remainingLives = _configuration.getInitialNumberOfTanks();
    private short _remainingAmmo;
    
    private class RemoveObjectTask extends TimerTask {
        private final String _objectID;

        public RemoveObjectTask(String objectID) {
            _objectID = objectID;
        }

        public void run() {
            removeObject(_objectID);
        }
    }
    
    /**
     * Tells the view to redraw when the model has changed
     */
    private final TimerTask _drawGraphicsTask = new TimerTask() {
        public void run() {
            if (_modelChanged) {
                _view.drawGraphics(_myTank, _othersTanksAndAllProjectiles.values(), _remainingLives, _remainingAmmo);
                _modelChanged = false;
            }
        }
    };

    
    /* Functions for use by Main */
    
    public void setNetwork(Network network) {
        if (_network != network) { //avoid an infinite loop
            _network = network;
            _network.setModel(this);
        }
    }

    public void setView(View view) {
        _view = view;
    }
    
    
    /* Functions for use by the controller */
    
    private boolean _taskSpawned;
    /**
     * Spawns my tank at a random position and azimuth
     * Also activates the model, making it start sending "draw graphics" signals to the view
     */
    public void spawnMyTank() {
        _remainingLives--;
        _myTank = new Tank(_random.nextFloat() * _configuration.getFieldWidth() / 2, _random.nextFloat() * _configuration.getFieldHeight() / 2, _random.nextFloat() * 360);
        _myTankID = newUniqueID();
        _network.addObject(_myTankID, _myTank);
        _remainingAmmo = _configuration.getInitialAmmunitionCount();
        _modelChanged = true;
        
        if (_taskSpawned == false) {
            //Unlike other tasks, the display doesn't have to be refreshed _exactly_ every 50ms.
            //So, this code will make the display refresh every 50ms on an ideal computer, but slower computers will have slower frame rates to prevent them from getting bogged down if they get behind.
            _timer.schedule(_drawGraphicsTask, 0, _configuration.getReactionTime());
            _network.addPlayer(_configuration.getPlayerName());
            _taskSpawned = true;
        }
    }

    /**
     * Pivots own tank left for a certain period of time.
     * @param time in milliseconds
     */
    public void pivotMyTankLeft(long time) {
        incrementMyTankAzimuth(-_configuration.getTankPivotRate() * time / 1000);
    }

    /**
     * Pivots own tank right for a certain period of time.
     * @param time in milliseconds
     */
    public void pivotMyTankRight(long time) {
        incrementMyTankAzimuth(_configuration.getTankPivotRate() * time / 1000);
    }

    /**
     * Moves own tank forward for a certain period of time.
     * @param time in milliseconds
     */
    public void moveMyTankForward(long time) {
        float speed = _configuration.getTankSpeed() * time / 1000;
        incrementMyTankPosition(speed * (float)Math.sin(_myTank.getAzimuthInRadians()), speed * (float)Math.cos(_myTank.getAzimuthInRadians()));
    }

    /**
     * Moves own tank backward for a certain period of time.
     * @param time in milliseconds
     */
    public void moveMyTankBackward(long time) {
        float speed = _configuration.getTankSpeed() * time / 1000;
        incrementMyTankPosition(-speed * (float)Math.sin(_myTank.getAzimuthInRadians()), -speed * (float)Math.cos(_myTank.getAzimuthInRadians()));
    }
    
    private long _lastMyTankFire = 0;
    public void fireMyTank() {
        //if the time between fires is at least the minimum time between fires
        if (_myTank.isDead() == false && _remainingAmmo > 0 && System.currentTimeMillis() - _lastMyTankFire >= (60 * 1000) / _configuration.getRateOfFire()) {
            final String projectileID = newUniqueID();
            Projectile projectile = new Projectile(_myTank.getPositionX(), _myTank.getPositionY(), _myTank.getAzimuth());
            
            addObject(projectileID, projectile, true); //add to model, play fire sound
            _network.addObject(projectileID, projectile); //add to network
            
            //get the projectile moving
            projectile.moveTask = new MoveProjectileTask(projectile, projectileID);
            _timer.schedule(projectile.moveTask, 0, _configuration.getReactionTime());
            _remainingAmmo--;
            _lastMyTankFire = System.currentTimeMillis();
            
            _modelChanged = true;
        }
    }
    
    
    /* Functions for use by the network component */
    
    public void addPlayer() {
        //send information about own objects for the new player's benefit
        _network.addObject(_myTankID, _myTank);
        for (Map.Entry<String, BattlezoneObject> entry : _othersTanksAndAllProjectiles.entrySet()) {
            String objectID = entry.getKey();
            if (isMine(objectID)) {
                _network.addObject(objectID, entry.getValue());
            }
        }
    }
    
    public void killObject(String id) {
        if (id.equals(_myTankID)) {
            _myTank.die();
        } else {
            _othersTanksAndAllProjectiles.get(id).die();
        }
        scheduleObjectRemoval(id); //everything must get removed even if it's owner is lazy and doesn't clean up after themself
        _view.playKillSound();
        _modelChanged = true;
    }

    public void addObject(String objectID, BattlezoneObject object) {
        addObject(objectID, object, false);
    }
    public void addObject(String objectID, BattlezoneObject object, boolean playFireSound) {
        if (!objectID.equals(_myTankID)) { //don't allow adding self as an enemy tank. That leads to tank suicide.
            if (_othersTanksAndAllProjectiles.get(objectID) == null) { //When an ENTER command is received all objects are re-added. Ignore ADD commands for objects that have already been added.
                _othersTanksAndAllProjectiles.put(objectID, object);
                _view.addObject(object);
                if (object.getClass() == Projectile.class) {
                    scheduleObjectRemoval(objectID); //projectiles expire after 3 seconds even if their owner exits the game
                }
                if (playFireSound) {
                    _view.playFireSound();
                }
                _modelChanged = true;
            }
        }
    }

    public void removeObject(String objectID) {
        if (objectID.equals(_myTankID)) { //removed own tank
            if (_remainingLives > 0) {
                spawnMyTank();
            } else { //if that was your last life, exit the program
                _network.removePlayer(_configuration.getPlayerName());
                System.exit(0);
            }
        } else { //removed another's tank
            BattlezoneObject object = _othersTanksAndAllProjectiles.get(objectID);
            //it is possible that the object was scheduled for removal more than once (for example, from a tank colliding with a tank that is already dead)
            //do not try to remove objects that have already been removed.
            if (object != null) {
                object.dispose();
                _othersTanksAndAllProjectiles.remove(objectID);
            }
        }
        _modelChanged = true;
    }

    public void updateObjectPosition(String objectID, float x, float y, float azimuth) {
        if (!objectID.equals(_myTankID)) { //ignore reflected UPDATE requests on own tank
            BattlezoneObject object = _othersTanksAndAllProjectiles.get(objectID);
            if (object != null) { //defensive programming: guard against networking flukes
                if (isMine(objectID) == false) { //own objects are updated internally - updating them through the network actually sets them back
                    object.setPositionAndAzimuth(x, y, azimuth);
                    _modelChanged = true;
                }
            } else { //if we receive a signal to update an object that isn't there, an ADD command was probably lost a while back.
                //so, resend the ENTER command to make the other computers tell this one what objects are there
                _network.addPlayer(_configuration.getPlayerName());
            }
        }
    }
    
    
    /**
     * A MoveProjectileTask is placed in each Projectile to allow them limited access to the model.
     * The MoveProjectileTask allows each projectile to cancel its movement when the projectile is dead.
     */
    public class MoveProjectileTask extends TimerTask {
        private final Projectile _projectile;
        private final String _projectileID;

        public MoveProjectileTask(Projectile projectile, String projectileID) {
            _projectile = projectile;
            _projectileID = projectileID;
        }

        public void run() {
            _projectile.incrementPosition(_configuration.getProjectileVelocity() * (float) Math.sin(_projectile.getAzimuthInRadians()) * _configuration.getReactionTime() / 1000, _configuration.getProjectileVelocity() * (float) Math.cos(_projectile.getAzimuthInRadians()) * _configuration.getReactionTime() / 1000);
            _network.updateObjectPosition(_projectileID, _projectile);
            
            //check for collision with a tank
            checkForCollision(_projectile, _projectileID);

            //I'm not going to worry about projectiles flying off the edge of the field. They won't get far before expiring.
            _modelChanged = true;
        }
    }
    

    /* Private functions */
    
    /**
     * Increments my tank's position, making sure that it doesn't go beyond the field boundary.
     * @param x
     * @param y
     */
    private void incrementMyTankPosition(float xBy, float yBy) {
        if (_myTank.isDead() == false) {
            float xIncrement = 0;
            float yIncrement = 0;

            if (Math.abs(_myTank.getPositionX() + xBy) < _configuration.getFieldWidth() / 2) {
                xIncrement = xBy;
            }
            if (Math.abs(_myTank.getPositionY() + yBy) < _configuration.getFieldHeight() / 2) {
                yIncrement = yBy;
            }
            _myTank.incrementPosition(xIncrement, yIncrement);
            _network.updateObjectPosition(_myTankID, _myTank);
            _view.playMoveSound();
            checkForCollision(_myTank, _myTankID);
            _modelChanged = true;
        }
    }

    private void incrementMyTankAzimuth(float by) {
        if (_myTank.isDead() == false) {
            _myTank.incrementAzimuth(by);
            _network.updateObjectPosition(_myTankID, _myTank);
            _view.playMoveSound();
            _modelChanged = true;
        }
    }

    /**
     * Schedules an object to be removed after 3 seconds.
     */
    private void scheduleObjectRemoval(String objectID) {
        _timer.schedule(new RemoveObjectTask(objectID), 3000);
    }

    /**
     * Checks if the object has collided with a tank and if so handles it appropriately.
     * The model only worries about its own objects - objects of other players are manipulated remotely via the network.
     * @param object
     */
    private void checkForCollision(BattlezoneObject object, String objectID) {
        for (Map.Entry<String, BattlezoneObject> entry : _othersTanksAndAllProjectiles.entrySet()) {
            BattlezoneObject otherObject = entry.getValue();
            if (otherObject.getClass() != Projectile.class) { //projectiles cannot impact themselves
                if (Math.abs(object.getPositionX() - otherObject.getPositionX()) < 4 && Math.abs(object.getPositionY() - otherObject.getPositionY()) < 4) {
                    final String otherObjectID = entry.getKey();

                    scheduleObjectRemoval(objectID);
                    scheduleObjectRemoval(otherObjectID);
                    object.die();
                    otherObject.die();
                    _network.killObject(objectID);
                    _network.killObject(otherObjectID);
                    _view.playKillSound();
                }
            }
        }
    }
    
    private Long _lastObjectNumber = 0l;
    /**
     * Returns a unique ID for the creation of a new object
     * @return
     */
    private String newUniqueID() {
        _lastObjectNumber++;
        return _configuration.getPlayerName() + '#' + _lastObjectNumber;
    }
    
    private boolean isMine(String objectID) {
        return objectID.startsWith(_configuration.getPlayerName() + '#');
    }
}